[iOS 8] UIVisualEffectView + UIDynamicAnimator で通知センター風 UI を実装する
磨りガラス風 View が実装できるようになったので
iOS 8 から UIVisualEffectView を使うと、磨りガラス風の View が作れるようになりました。いままで同様の実装を行うにはライブラリを利用しなければいけなかったのですが、標準 API だけで利用できるようになったのはとてもありがたいですね。
さて、UIVisualEffectView を使ってどのような UI を作りましょうか?まず始めに思いつくのが、そう、通知センターですよね!(勝手)ということで UIVisualEffectView を使って通知センター風 UI を実装してみました。通知センターを引っ張って出してくるときのアニメーションは重力を付けてあげれば実装できそうなので UIDynamicAnimator を使っています。
なお、UIVisualEffectView の概要は次の記事をご覧いただければと思います。
[iOS 8]UIVisualEffectViewを使ってすりガラス効果を実現する | Developers.IO
実装してみる
それでは実装してみます。実装内容をすべて掲載しています。少し長く見えますが悪しからず。
import UIKit
class BlurViewController: UIViewController {
// 表示する最小サイズ let minHeight: CGFloat = 50 // 方向を変更する境界位置の調整値 let boundaryAdjustmentY: CGFloat = 100
// UIDynamicAnimator var animator: UIDynamicAnimator! var collision: UICollisionBehavior! var gravity: UIGravityBehavior!
// UIVisualEffectView var blurView: UIVisualEffectView! var vibrancyView: UIVisualEffectView!
// 上に吸着するか、下に吸着するか var isDirectionTop: Bool = true // タッチ中か否か var touching: Bool = false // タッチ開始時の指の位置 var startLocation: CGPoint! // タッチ開始時のblurViewの位置 var startPosition: CGPoint!
override func viewDidLoad() { super.viewDidLoad()
// 背景 let image = UIImage(named: "Bg01") let background = UIImageView(image: image) self.view.addSubview(background)
addBlurView() addVibrancyEffectView() }
// MARK: - BlurView
func addBlurView() {
let width = self.view.frame.size.width let height = self.view.frame.size.height
// ① BlurView の生成
blurView = blurEffectView(fromBlurStyle: .Dark, frame: self.view.frame) blurView.frame.origin.y = minHeight - height blurView.contentView.userInteractionEnabled = true self.view.addSubview(blurView)
setDirection(false) }
func addVibrancyEffectView() {
// ② UIVibrancyEffect の View の作成
vibrancyView = vibrancyEffectView(fromBlurEffect: blurView.effect as UIBlurEffect, frame: self.view.frame) blurView.contentView.addSubview(vibrancyView)
var label = UILabel(frame: CGRectMake(0, 0, 300, 100)) label.text = "通知センターもどきだよ" label.font = UIFont.boldSystemFontOfSize(24) label.textAlignment = .Center label.center = CGPointMake(vibrancyView.frame.size.width / 2, vibrancyView.frame.size.height / 2) vibrancyView.contentView.addSubview(label) }
func blurEffectView(fromBlurStyle style: UIBlurEffectStyle, frame: CGRect) -> UIVisualEffectView { let effect = UIBlurEffect(style: style) let blurView = UIVisualEffectView(effect: effect) blurView.frame = frame return blurView }
func vibrancyEffectView(fromBlurEffect effect: UIBlurEffect, frame: CGRect) -> UIVisualEffectView { let vibrancyEffect = UIVibrancyEffect(forBlurEffect: effect) let vibrancyView = UIVisualEffectView(effect: vibrancyEffect) vibrancyView.frame = frame return vibrancyView }
// MARK: - UIGravityBehavior
func setDirection(top: Bool) {
// ③ UIGravityBehavior の適用
if gravity == nil {
let width = self.view.frame.size.width let height = self.view.frame.size.height
// UIDynamicAnimator の作成 (領域の作成) animator = UIDynamicAnimator(referenceView: self.view) collision = UICollisionBehavior(items: [blurView]) collision.addBoundaryWithIdentifier("top", fromPoint: CGPointMake(0, minHeight - height), toPoint: CGPointMake(width, minHeight - height)) collision.addBoundaryWithIdentifier("bottom", fromPoint: CGPointMake(0, height), toPoint: CGPointMake(width, height)) collision.addBoundaryWithIdentifier("left", fromPoint: CGPointMake(0, minHeight - height), toPoint: CGPointMake(0, height)) collision.addBoundaryWithIdentifier("left", fromPoint: CGPointMake(width, minHeight - height), toPoint: CGPointMake(width, height)) animator.addBehavior(collision)
gravity = UIGravityBehavior(items: [blurView]) gravity.magnitude = 1.0 animator.addBehavior(gravity) } // 重力を変更する gravity.gravityDirection = top ? CGVectorMake(0.0, -3.0) : CGVectorMake(0.0, 3.0) collision.addItem(blurView) gravity.addItem(blurView) }
// MARK: - Touch
override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!) { println(__FUNCTION__) let touch: UITouch = touches.anyObject() as UITouch if !touch.view.isEqual(vibrancyView.contentView) { // vibrancyViewでなければ何もしない touch.view.nextResponder() return } // 開始位置を保持する touching = true startLocation = touches.anyObject().locationInView(self.view) startPosition = blurView.frame.origin println("start location : \(startLocation)")
// ④ 重力を一旦外す
gravity.removeItem(blurView) collision.removeItem(blurView) }
override func touchesMoved(touches: NSSet!, withEvent event: UIEvent!) { println(__FUNCTION__) // タッチ位置の移動に合わせてblurViewを移動させる if touching { let currentLocation: CGPoint = touches.anyObject().locationInView(self.view) let gapY = currentLocation.y - startLocation.y println("current location : \(currentLocation)") println("gap : \(gapY)") if (startPosition.y + gapY + blurView.frame.size.height > blurView.frame.size.height) { // Y座標が最大値を超えないようにする blurView.frame.origin.y = 0; } else { blurView.frame.origin.y = startPosition.y + gapY } } }
override func touchesEnded(touches: NSSet!, withEvent event: UIEvent!) { println(__FUNCTION__) if touching { touching = false // どちらに吸着するか判定する let point: CGPoint = touches.anyObject().locationInView(self.view) let boundaryY = isDirectionTop ? boundaryAdjustmentY : self.view.frame.size.height - boundaryAdjustmentY isDirectionTop = point.y < boundaryY // ⑤ 重力を再設定する setDirection(isDirectionTop) } } override func touchesCancelled(touches: NSSet!, withEvent event: UIEvent!) { println(__FUNCTION__) if touching { touching = false // どちらに吸着するか判定する let point: CGPoint = touches.anyObject().locationInView(self.view) let boundaryY = isDirectionTop ? boundaryAdjustmentY : self.view.frame.size.height - boundaryAdjustmentY isDirectionTop = point.y < boundaryY // 重力を再設定する setDirection(isDirectionTop) } } } [/swift]
ポイントだけ解説します!
① UIBlurEffect の View の作成
まずは磨りガラス風の View を生成します。磨りガラス風の View を生成するには、UIBlurEffect と UIVisualEffectView を使います。画面いっぱいのサイズで作成し、UIViewController の view に addSubView します。
② UIVibrancyEffect の View の作成
BlurView の上に重ねる View を生成します。UIVibrancyEffect を適用すると、ぼかしが少し透けた View を表示できます。contentView の子の View が適用されるので、ここでは Label を追加しています。
③ UIGravityBehavior の適用
①, ②で作成した View に重力を加えます。UICollisionBehavior は重力を加える View の領域を指定するクラスです。画面内に収まる場合は不要ですが、通知センター風の UI を作るには範囲外から View を表示する必要があるので、画面外にはみ出るように指定しています。上部に空きがあるイメージです。
④ 重力を一旦外す
ドラッグ中に重力をかけてしまうとおかしな挙動になるので、タッチしたタイミングで重力を一旦外しています。ドラッグ中は Y 座標の値だけ追従するようにしています。
⑤ 重力を再設定する
ドラッグが終わったら(またはキャンセルされたら)改めて重力を加えます。ドラッグ終了時のタップ位置が画面の下のほうだったら下部に重力をかけ、上のほうだったら逆に上部に重力をかけています。
実行すると次のように動作します。
まとめ
本物の通知センターと比較すると細かい動きは違ってしまいましたが、近い感じには実装できたかなぁと思います。UIVisualEffectView の実装例として参考にしていただければ幸いです。
バグもあるかも知れないので、気になる所があればご指摘いただければと思います!